import simpy
from numpy import random
import math
import queue



class MicroserviceSystem():
    def __init__(self,lamda_norm_f,lamda_error_f,lamda_com_f,lamda_out_f,num_server_f,time_deadline_f,start_record_time_f):
        self.lamda_norm=lamda_norm_f
        self.lamda_error=lamda_error_f

        self.lamda_com=lamda_com_f
        self.lamda_out=lamda_out_f
        self.num_server=num_server_f

        self.time_deadline=time_deadline_f
        self.start_recored_time=start_record_time_f

        self.rou=self.lamda_com/self.lamda_out/self.num_server
        self.r=self.lamda_com/self.lamda_out
        self.all_out_num=0
        self.time_succeed_num=0
        self.time_error_succeed_num=0
        self.queue = queue.Queue()



    def get_math_reliability(self):
        prob_time=0 #self.get_prob_with_time_deadline(self.time_deadline)
        prob_error=self.get_prob_with_error(self.time_deadline)
        return prob_time,prob_error
    def get_prob_with_time_deadline(self,deadline):
        sum_temp=0
        for i in range(self.num_server):
            sum_temp+=pow(self.r,i)/math.factorial(i)
        p_0=1/(pow(self.r,self.num_server)/math.factorial(self.num_server)/(1-self.rou)+sum_temp)

        wq_0=1-pow(self.r,self.num_server)*p_0/math.factorial(self.num_server)/(1-self.rou)
        # a1=(self.num_server*(1-self.rou)-wq_0)
        # a2=(1 - pow(math.e, -1 * self.lamda_out * deadline))
        denominator=(self.num_server*(1-self.rou)-1)

        if denominator==0:
            print("denominator is zero")
            return 0
        part1 = (self.num_server * (1 - self.rou) - wq_0) * (1 - pow(math.e, -1 * self.lamda_out * deadline)) / (self.num_server * (1 - self.rou) - 1)
        part2=(1-wq_0)*(1-pow(math.e,-1*deadline*(self.num_server*self.lamda_out-self.lamda_com)))/(self.num_server*(1-self.rou)-1)
        prob=part1-part2
        return prob
    
    def get_prob_with_error(self,deadline):
        sum_temp = 0
        for i in range(self.num_server):
            sum_temp += pow(self.r, i) / math.factorial(i)
        if abs(self.rou-1) < 0.0000000001 :
            print("rou is zero")
            return -1
        p_0 = 1 / (pow(self.r, self.num_server) / math.factorial(self.num_server) / (1 - self.rou) + sum_temp)

        wq_0 = 1 - pow(self.r, self.num_server) * p_0 / math.factorial(self.num_server) / (1 - self.rou)
        denominator = (self.num_server * (1 - self.rou) - 1)

        if abs(denominator) < 0.0000000001:
            print("denominator is zero")
            return -1
        part1=(self.num_server*(1-self.rou)-wq_0)*self.lamda_out*(math.exp(-1*(self.lamda_norm+self.lamda_out)*deadline)-1)\
            /(self.num_server*(1-self.rou)-1)/(-1*(self.lamda_norm+self.lamda_out))
        part2=(1-wq_0)*(self.num_server*self.lamda_out-self.lamda_com)*(math.exp(-1*(self.lamda_out*self.num_server-self.lamda_com+self.lamda_norm)*deadline)-1)\
            /(self.num_server*(1-self.rou)-1)/(-1*(self.num_server*self.lamda_out-self.lamda_com+self.lamda_norm))
        prob_start = self.lamda_error / (self.lamda_norm + self.lamda_error)
        return (part1-part2)*prob_start

        # prob_start=self.lamda_error/(self.lamda_norm+self.lamda_error)
        # prob_continue=self.lamda_out/(self.lamda_norm+self.lamda_out)
        # return prob_start*prob_continue
        # prob_start_point=(math.pow(self.lamda_error,2)-self.lamda_norm*self.lamda_error-self.lamda_norm*self.lamda_error*math.log(self.lamda_error/self.lamda_norm))\
        #      /math.pow(self.lamda_norm-self.lamda_error,2)
        # prob_later_len=self.lamda_out/(self.lamda_norm+self.lamda_out)
        # return prob_start_point

    def get_time_with_exponent(self,lamda):
        return random.exponential(scale=1 / lamda, size=(1, 1))[0, 0]


    def init_system(self,env):
        self.microservice=[MicroService(i,self.lamda_norm,self.lamda_error) for i in range(self.num_server)]
        for i in range(self.num_server):
            env.process(self.microservice_error_judge(env,i))

    def microservice_error_judge(self,env,ms_id):
        change_time=self.microservice[ms_id].next_time_point
        while True:
            yield env.timeout(change_time)
            change_time = -1
            if change_time<0:
                if self.microservice[ms_id].state=="norm":
                    change_time=self.get_time_with_exponent(self.lamda_error)
                    self.microservice[ms_id].set_state("error",env.now+change_time)
                    # print("time now:",env.now,"ms ",ms_id," state from norm to error, continue time:", change_time, "next time:", env.now+change_time)
                elif self.microservice[ms_id].state=="error":
                    change_time = self.get_time_with_exponent(self.lamda_norm)
                    self.microservice[ms_id].set_state("norm", env.now + change_time)
                    # print("time now:", env.now, "ms ", ms_id, " state from error to norm, continue time:", change_time, "next time:", env.now+change_time)
                else:
                    print("microservice_error_judge wrong!")
                    exit(-1)



    #checked
    def get_ms_with_min_queue(self):
        aim_id=-1
        min_queue=9999999
        start_time=9999999
        for i in range(self.num_server):
            if self.microservice[i].queue.qsize()<min_queue or \
                    (self.microservice[i].queue.qsize()==min_queue and self.microservice[i].start_time<start_time):
                aim_id=i
                min_queue=self.microservice[i].queue.qsize()
                start_time=self.microservice[i].start_time
        return aim_id



    def customer_com(self,env):
        request_id_order=0
        while True:
            time_customer = self.get_time_with_exponent(self.lamda_com)
            yield env.timeout(time_customer)
            #get ms with min queue size
            deal_time=self.get_time_with_exponent(self.lamda_out)
            request_id_order+=1
            request_new=Request(request_id_order,env.now,deal_time)
            self.add_request_to_ms_queue(env,request_new)
            request_id_order+=1

            # print("now time:", env.now, "customer come!\t queue len:", self.queue.qsize())
    def any_ms_free(self):
        for i in range(self.num_server):
            if not self.microservice[i].isbusy:
                return True,i
        return False,-1
    def add_request_to_ms_queue(self,env,request_new):
        self.queue.put(request_new)
        any_free,id=self.any_ms_free()
        if any_free:
            env.process(self.server_deal(env,id))

    def server_deal(self,env, ms_id):
        if self.queue.qsize() > 0:
            self.microservice[ms_id].isbusy=True
            request=self.queue.get()
            deal_time=request.deal_time
            if self.microservice[ms_id].state == "norm" and self.microservice[ms_id].next_time_point>=env.now+deal_time:
                request.noerror=True

            # print("now time:", env.now, "server deal!\t queue len:", queue.get_size())
            self.microservice[ms_id].start_time=env.now

            yield env.timeout(deal_time)
            #记录完成数量和成功与否
            if env.now>self.start_recored_time:
                self.all_out_num+=1

                a=env.now-request.arrive_time
                if env.now-request.arrive_time<=self.time_deadline: #request.noerror==True and
                    self.time_succeed_num+=1
                    if request.noerror == True:
                        self.time_error_succeed_num+=1
                    # if request.noerror ==False:
                    #     print("a")

            if self.queue.qsize() == 0:
                self.microservice[ms_id].isbusy=False
                return
            else:
                env.process(self.server_deal(env, ms_id))

    def run(self,until_time):
        env = simpy.Environment()
        self.init_system(env)
        env.process(self.customer_com(env))
        env.run(until=until_time)

    def get_sim_reliability(self):
        prob_time=self.time_succeed_num / self.all_out_num
        prob_time_error=self.time_error_succeed_num / self.all_out_num
        return prob_time,prob_time_error




class Request():
    def __init__(self,request_id,arrive_time_f,deal_time_f):
        self.id=request_id
        self.arrive_time=arrive_time_f
        self.deal_time=deal_time_f
        self.noerror=False

class MicroService():
    def __init__(self,ms_id,lamda_norm_f,lamda_error_f):

        self.id=ms_id
        self.lamda_norm=lamda_norm_f
        self.lamda_error=lamda_error_f
        self.state="norm"
        self.next_time_point=random.exponential(scale=1 / lamda_norm_f, size=(1, 1))[0, 0]
        self.start_time = 0
        self.isbusy=False

    def get_state(self):
        return self.state,self.next_time_point
    def set_state(self,new_state,new_time_point):
        self.state=new_state
        self.next_time_point=new_time_point

    def set_start_time(self,start_time_f):
        self.start_time=start_time_f

if __name__ == "__main__":
    lamda_norm = 0.00001
    lamda_error = 0.00001

    lamda_com = 10
    lamda_out = 100000
    num_server = 10

    time_deadline = 2


    start_record_time=100
    ms_system=MicroserviceSystem(lamda_norm,lamda_error,lamda_com,lamda_out,num_server,time_deadline,start_record_time)
    ms_system.run(10000)
    math_prob_time,math_prob_time_error=ms_system.get_math_reliability()
    sim_prob_time,sim_prob_time_error=ms_system.get_sim_reliability()
    print("数学计算得到的可靠性：(only time)",math_prob_time,"\t(time and error)",math_prob_time_error )
    print("仿真推演得到的可靠性：(only time)",sim_prob_time,"\t(time and error)",sim_prob_time_error)
    print("system end!")